package org.filecollector.filecontroller.virtualcatalogue;

import lombok.Getter;
import lombok.NonNull;
import org.filecollector.filecontroller.virtualcatalogue.groupname.GroupName;
import org.filecollector.filecontroller.virtualcatalogue.virtualfile.*;

import java.io.File;
import java.nio.file.Path;
import java.util.LinkedHashSet;
import java.util.Optional;
import java.util.OptionalLong;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;

/**
 * Class that implements {@link VirtualCatalogue} interface
 */
class VirtualCatalogueClass implements VirtualCatalogue {
    @Getter
    @NonNull VirtualCatalogueDependencies dependencies;
    @Getter
    @NonNull VirtualCatalogueDao data;

    protected VirtualCatalogueClass(@NonNull VirtualCatalogueDao data, @NonNull VirtualCatalogueDependencies dependencies) {
        this.dependencies = dependencies;
        this.data = data;
    }

    protected OptionalLong writeToFile(Long id, char[] buffer, boolean append) {
        if (!checkDirectory()) {
            return OptionalLong.empty();
        }
        if (!exists(id)) {
            dependencies.warner().accept(String.format("File with id %d wasn't created. Use touch().", id));
            return OptionalLong.empty();
        }
        if (!VirtualCatalogueStaticMethods.checkAndFixWriteability(
                data.FileMap().get(id).getPath().toFile(),
                dependencies.warner()
        )) {
            return OptionalLong.empty();
        }
        return preventThrowing(
                () -> VirtualCatalogueStaticMethods.writeDirectly(
                        id,
                        buffer,
                        () -> data.FileMap().get(id),
                        append,
                        dependencies.troubleHuntPutter(),
                        dependencies.debugger()
                ),
                OptionalLong::empty
        );
    }

    /**
     * List tracking directory and construct HashMap of id-VirtualFile
     * @return redirect to this object
     */
    protected VirtualCatalogueClass startingInitialization() {
        Function<File, VirtualFileDependencies> supplier = (File file) -> {
            var id = Long.valueOf(file.getName(), 16);
            return dependencies.standardDependenciesVFProducer().apply(id);
        };
        VirtualCatalogueStaticMethods.startInitialization(
                data.directory(),
                dependencies.warner(),
                (File file) -> VirtualCatalogueStaticMethods.troubleHunt(
                        () -> VirtualFilesFactory.openExistingFile(
                                file,
                                supplier.apply(file)
                        ),
                        (InvalidFileException e) -> {
                            dependencies.warner().accept(e.getMessage());
                            dependencies.troubleHuntPutter().accept(e);
                        }
                ),
                dependencies.troubleHuntPutter(),
                (VirtualFile value) -> data.FileMap().put(value.getId(), value)
        );
        return this;
    }

    /**
     * Preventing throw
     * @param func executable function
     * @param ifThrown function that supplies if something goes wrong
     * @return object of T
     * @param <T> any class
     */
    private <T> T preventThrowing(Supplier<T> func, Supplier<T> ifThrown) {
        return VirtualCatalogueStaticMethods.preventThrowing(func, dependencies.warner(), ifThrown);
    }

    /**
     * Check existence of directory and if something wrong sending message to errors stream
     * @return false if directory doesn't exists or unreadable
     */
    private boolean checkDirectory() {
        return VirtualCatalogueStaticMethods.checkDirectory(data.directory(), dependencies.error(), dependencies.warner());
    }

    /**
     * Check if file exists
     * @param id id of file
     * @return true if file exists; false in other cases
     */
    @Override
    public boolean exists(Long id) {
        if (!checkDirectory()) {
            return false;
        }
        dependencies.debugger().accept(String.format("Checking existing of File with id %d", id));
        return data.FileMap().containsKey(id);
    }

    /**
     * read char[] of maximum {@code size} from file by id
     * @param id id of file
     * @param size maximum size of file
     * @return Optional of char[] of size or less
     * @implNote <li>if size of file is less than {@code size} returns char[] of file's size</li>
     * <li>if something goes wrong {@link Optional#empty()} returns</li>
     */
    @Override
    public Optional<char[]> readDirectly(Long id, int size) {
        if (!checkDirectory()) {
            return Optional.empty();
        }
        if (!exists(id)) {
            dependencies.warner().accept(String.format("File with id %d wasn't created. Nothing to read.", id));
            return Optional.empty();
        }
        if (!VirtualCatalogueStaticMethods.checkAndFixReadability(
                data.FileMap().get(id).getPath().toFile(),
                dependencies.warner()
            )
        ) {
            return Optional.empty();
        }
        return preventThrowing(
                () -> VirtualCatalogueStaticMethods.readDirectly(
                        id,
                        size,
                        () -> data.FileMap().get(id),
                        dependencies.troubleHuntPutter(),
                        dependencies.debugger()
                ),
                Optional::empty
        );
    }

    /**
     * Updates file and add buffer
     * @param id id of updating file
     * @param buf adding buffer
     * @return id or Optional.empty()
     */
    @Override
    public OptionalLong addDirectly(Long id, char[] buf) {
        return writeToFile(id, buf, true);
    }

    /**
     * Get Optional of path of file
     * @param id id of file
     * @return Path of tracking file
     * @apiNote returns {@link Optional#empty()} if something goes wrong
     */
    @Override
    public Optional<Path> getPath(Long id) {
        if (!checkDirectory()) {
            return Optional.empty();
        }
        if (!exists(id)) {
            dependencies.warner().accept(String.format("File with id %d wasn't created. No Path.", id));
            return Optional.empty();
        }
        return preventThrowing(
                () -> VirtualCatalogueStaticMethods.getPath(
                        id,
                        () -> data.FileMap().get(id),
                        dependencies.troubleHuntPutter(),
                        dependencies.debugger()
                ),
                Optional::empty
        );
    }

    /**
     * Calculates checksum
     * @param id id of file
     * @return checksum of file
     * @apiNote returns {@link OptionalLong#empty()} if something goes wrong
     */
    @Override
    public OptionalLong getCheckSum(Long id) {
        if (!checkDirectory()) {
            return OptionalLong.empty();
        }
        if (!exists(id)) {
            return OptionalLong.empty();
        }
        if (!VirtualCatalogueStaticMethods.checkAndFixReadability(
                data.FileMap().get(id).getPath().toFile(),
                dependencies.warner())
        ) {
            return OptionalLong.empty();
        }
        return preventThrowing(
                () -> VirtualCatalogueStaticMethods.getCheckSum(
                        id,
                        () -> data.FileMap().get(id),
                        dependencies.troubleHuntPutter(),
                        dependencies.debugger()
                ),
                OptionalLong::empty
        );
    }


    /**
     * Write from char[] buffer to file
     * @param id id of file
     * @param buf writing buffer
     * @return Id of file if success
     * @apiNote <li>Returns {@link OptionalLong#empty()} if something goes wrong</li>
     */
    @Override
    public OptionalLong writeDirectly(Long id, char[] buf) {
        return writeToFile(id, buf, false);
    }

    /**
     * Touches file
     * @param id id of file
     * @return id of file if success
     * @apiNote Return {@link OptionalLong#empty()} if something goes wrong
     */
    @Override
    public OptionalLong touch(Long id) {
        if (!checkDirectory()) {
            return OptionalLong.empty();
        }
        return preventThrowing(
                () -> VirtualCatalogueStaticMethods.touch(
                    id,
                    () -> {
                        var ret =data.FileMap().put(
                            id,
                            VirtualFilesFactory.createOrOpenFile(
                                data.directory(),
                                id,
                                dependencies.standardDependenciesVFProducer().apply(id)
                            )
                        );
                        return ret==null? 0L : ret.getId();
                        },
                    dependencies.troubleHuntPutter(),
                    (Long locId) -> data.FileMap().containsKey(locId),
                    dependencies.informer()
                ),
                OptionalLong::empty
        );
    }

    /**
     * Check can id used in this virtual catalogue
     * @param id id of file
     * @return true if id can be in catalogue diapason, false in other cases.
     */
    @Override
    public boolean checkId(Long id) {
        return data.groupName().matchesId(id);
    }

    /**
     * Remove file
     * @param id id of file
     * @return true if file removed successfully
     */
    @Override
    public boolean remove(Long id) {
        if (!data.FileMap().containsKey(id)) {
            return false;
        }
        return VirtualCatalogueStaticMethods.remove(
                () -> {
                    var ret = data.FileMap().get(id);
                    data.FileMap().remove(id);
                    return ret;
                }
        );
    }

    /**
     * Removing tracking
     * @return true if deletion was success
     */
    @Override
    public boolean removeCatalogue() {
        dependencies.informer().accept(
                String.format(
                        "Directory %s is deleting",
                        data.groupName().getName()
                )
        );
        return preventThrowing(
                () -> {
                    try {
                        return dependencies.recursiveRemover().apply(data.directory());
                    } catch (SecurityException e) {
                        dependencies.troubleHuntPutter().accept(
                            new InvalidFileException(
                                "Has no access to directory",
                                FileTroubles.UNREADABLE,
                                data.directory().toPath()
                            )
                        );
                    }
                    return false;
                },
                () -> false
        );
    }

    /**
     * @return all file ids
     */
    @Override
    public Long[] listIds() {
        return data.FileMap().keySet().toArray(new Long[0]);
    }

    /**
     * @return array of free id diapasons
     */
    @Override
    public Long[][] listFreeDiapasons() {
        Long minId = data.groupName().getMinId();
        Long maxId = data.groupName().getMaxId();
        var occupiedId = data.FileMap().keySet().stream().sorted().collect(Collectors.toCollection(LinkedHashSet::new));
        return VirtualCatalogueStaticMethods.findFreeDiapasons(minId, maxId, occupiedId).toArray(new Long[][]{});
    }

    /**
     * @return GroupName of Catalogue
     */
    @Override
    public GroupName getName() {
        return data.groupName();
    }

}
